Skip to content

使用 HttpClient 呼叫 WebService

TLDR

  • 在無法加入 Web 參考的環境下,可透過 HttpClient 手動建構 SOAP 1.2 訊息來呼叫 WebService。
  • 建議優先使用 dotnet-svcutil 從 WSDL 產生強型別 Client,手刻 SOAP 僅適用於無法取得可用 WSDL 的極端情境。
  • 使用 XmlSerializer 處理複雜型別的序列化與反序列化,可確保參數傳遞與回傳值解析的正確性。
  • 應避免頻繁建立 HttpClient 實例,建議使用 IHttpClientFactory 或設定 PooledConnectionLifetime 以管理連線生命週期。
  • SOAP 1.2 規範的 Media Type 應為 application/soap+xml

為什麼需要手刻 HttpClient 呼叫 WebService

在 .NET 開發中,通常使用 Visual Studio 的「加入 Web 參考」功能即可完成 WebService 呼叫。然而,若開發環境無法連線至 WebService 導致無法加入參考,或 WSDL 檔案遺失、解析失敗時,則需要採取替代方案。

建議優先採用的方式:dotnet-svcutil

在 .NET Core 之後,正規做法是使用 dotnet-svcutil(或 Visual Studio 的「WCF Web Service Reference」)。此工具僅需本地的 WSDL 檔案即可產生強型別 Client,不需直接連線至服務端點。

注意事項

產生的 Client 是該份 WSDL 的快照。若 WebService 介面有異動,必須重新取得 WSDL 並重新產生 Client。


使用 HttpClient 與 XmlSerializer 實作 SOAP 呼叫

當無法取得可用 WSDL 時,可透過 HttpClient 發送 SOAP 1.2 請求,並利用 XmlSerializer 處理複雜物件的轉換。

實作邏輯

此方法的核心在於將物件序列化為 XML,並嵌入 SOAP Envelope 中。

csharp
public static class WebServiceUtils {
    // 建議使用 IHttpClientFactory 管理生命週期,避免 Socket 耗盡或 DNS 解析問題
    private static readonly HttpClient httpClient = new HttpClient();

    public static async Task<TResponse> ExecuteAsync<TResponse>(string uri, string method, IDictionary<string, object> arguments, string @namespace = "http://tempuri.org/") {
        XmlSerializerNamespaces serializerNamespaces = new XmlSerializerNamespaces(new[] { XmlQualifiedName.Empty });
        XmlWriterSettings settings = new XmlWriterSettings {
            Indent = true,
            OmitXmlDeclaration = true
        };

        string argsXml = string.Join("", arguments.Select(x => {
            Type type = x.Value.GetType();
            XmlSerializer _serializer = new XmlSerializer(type);
            StringBuilder sb = new StringBuilder();
            using (XmlWriter writer = XmlWriter.Create(sb, settings)) {
                _serializer.Serialize(writer, x.Value, serializerNamespaces);
                // 替換 Root 節點名稱為 Dictionary Key
                return Regex.Replace(sb.ToString(), $@"((?<=^<)(\w*)(?=>))|(?<=</)\w*(?=>$)", x.Key);
            }
        }));

        string soapXml = $@"
            <soap12:Envelope xmlns:xsi=""http://www.w3.org/2001/XMLSchema-instance"" xmlns:xsd=""http://www.w3.org/2001/XMLSchema"" xmlns:soap12=""http://www.w3.org/2003/05/soap-envelope"">
              <soap12:Body>
                <{method} xmlns=""{@namespace}"">
                    {argsXml}
                </{method}>
              </soap12:Body>
            </soap12:Envelope>
        ";

        // SOAP 1.2 建議使用 application/soap+xml
        StringContent content = new StringContent(soapXml, Encoding.UTF8, "application/soap+xml");
        using (HttpResponseMessage message = await httpClient.PostAsync(uri, content).ConfigureAwait(false)) {
            if (!message.IsSuccessStatusCode) {
                throw new HttpRequestException($"HTTP request failed with status code {message.StatusCode}: {message.ReasonPhrase}");
            }

            string result = await message.Content.ReadAsStringAsync().ConfigureAwait(false);

            XDocument xdoc = XDocument.Parse(result);
            XNamespace ns = @namespace;
            string resultTag = method + "Result";

            XElement xelement = xdoc.Descendants(ns + resultTag).Single();

            XmlSerializer serializer = new XmlSerializer(typeof(TResponse), new XmlRootAttribute(resultTag) { Namespace = @namespace });

            using (XmlReader reader = xelement.CreateReader()) {
                return (TResponse)serializer.Deserialize(reader);
            }
        }
    }
}

驗證結果

透過定義巢狀的 RequestResponse 類別進行測試,該實作能正確處理複雜型別的參數傳遞與回傳值解析。

webservice received request

client received response


異動歷程

    • 初版文件建立。
    • 修正 ExecuteAsync 參數型別筆誤(IDictionary<string, string> 應為 IDictionary<string, object>)。
    • 改寫 description 為實際的 HttpClient + SOAP 做法。
    • 補上對應的可執行範例連結。
    • 補充 HttpClient 生命週期與 SOAP 1.2 media type 的程式碼註解。
    • 補上 svcutil 從 WSDL 產生 client 的 tip,並標明手刻做法的適用情境。